package org.xx.armory.swing;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.LoggerFactory;
import org.xx.armory.swing.components.DialogPane;
import org.xx.armory.swing.components.ListItem;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleComponent;
import javax.accessibility.AccessibleContext;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import static java.util.Arrays.stream;
import static org.xx.armory.commons.Validators.greaterThanOrEqual;
import static org.xx.armory.commons.Validators.notNull;
import static org.xx.armory.swing.UIUtils.assertUIThread;

/**
 * 组件相关的工具类。
 */
public final class ComponentUtils {

    private static final int TABLE_COLUMN_MIN_WIDTH = 12;
    private static final int TABLE_COLUMN_MAX_WIDTH = 640;

    /**
     * 将指定的矩形在另一个矩形内居中，尺寸不变。
     *
     * @param inner
     *         内部矩形。
     * @param outer
     *         外部矩形。
     * @return 内部矩形的位置。
     */
    public static Point center(
            Rectangle inner,
            Rectangle outer
    ) {
        notNull(inner, "inner");
        notNull(inner, "outer");

        final double x = (outer.getWidth() - inner.getWidth()) / 2;
        final double y = (outer.getHeight() - inner.getHeight()) / 2;

        return new Point((int) x, (int) y);
    }

    /**
     * 将指定的矩形在另一个矩形内居中，尺寸不变。
     *
     * @param inner
     *         内部矩形的尺寸。
     * @param outer
     *         外部矩形的尺寸。
     * @return 内部矩形的位置（相对外部矩形）。
     */
    public static Point center(
            Dimension inner,
            Dimension outer
    ) {
        notNull(inner, "inner");
        notNull(inner, "outer");

        double x = (outer.width - inner.width) / 2;
        double y = (outer.height - inner.height) / 2;

        if (x < 0.001D) {
            x = 1D;
        }
        if (y < 0.001D) {
            y = 1D;
        }

        return new Point((int) x, (int) y);
    }

    /**
     * 设置表格的列宽。
     *
     * @param table
     *         待设置的表格。
     * @param columnsWidth
     *         每列的宽度，未指定的列采用最大列宽，如果指定的列宽超过最大列宽也采用最大列宽。
     * @throws IllegalArgumentException
     *         如果参数{@code table}是{@code null}或者参数{@code columnsWidth}是{@code null}。
     */
    public static void setTableColumnsWidth(
            JTable table,
            int... columnsWidth
    ) {
        notNull(table, "table");
        notNull(columnsWidth, "columnsWidth");

        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        final TableColumnModel columnModel = table.getColumnModel();
        for (int i = 0; i < columnModel.getColumnCount(); ++i) {
            final TableColumn column = columnModel.getColumn(i);
            int columnWidth = i < columnsWidth.length ? columnsWidth[i] : TABLE_COLUMN_MAX_WIDTH;
            if (columnWidth < 0 || columnWidth > TABLE_COLUMN_MAX_WIDTH) {
                columnWidth = 640;
            }
            if (columnWidth > 0) {
                column.setWidth(columnWidth);
                column.setMinWidth(TABLE_COLUMN_MIN_WIDTH);
                column.setPreferredWidth(columnWidth);
            }
        }
    }

    /**
     * 读取JFrame的位置属性。
     * <p>这些数据可以保存起来，将来恢复JFrame的位置、尺寸和状态。</p>
     *
     * @param frame
     *         待读取的JFrame。
     * @return JFrame的位置属性。
     * @throws IllegalArgumentException
     *         如果参数{@code frame}是{@code null}。
     */
    public static FrameBounds getFrameBounds(
            JFrame frame
    ) {
        notNull(frame, "frame");

        final Rectangle bounds = frame.getBounds();
        int status = frame.getExtendedState();
        if (status == JFrame.ICONIFIED) {
            status = JFrame.NORMAL;
        }
        return new FrameBounds(status, bounds.x, bounds.y, bounds.width, bounds.height);
    }

    public static void extend(
            Dimension d,
            int x,
            int y
    ) {
        notNull(d, "d");
        greaterThanOrEqual(x, "x", 0);
        greaterThanOrEqual(y, "y", 0);

        d.width += x;
        d.height += y;
    }

    public static void extend(
            Rectangle r,
            Insets insets
    ) {
        r.x -= insets.left;
        r.y -= insets.top;
        r.width += insets.left + insets.right;
        r.height += insets.top + insets.bottom;
    }

    public static void shrink(
            Dimension d,
            int x,
            int y
    ) {
        notNull(d, "d");
        greaterThanOrEqual(x, "x", 0);
        greaterThanOrEqual(y, "y", 0);

        d.width -= x;
        d.height -= y;

        if (d.width < 0) {
            d.width = 0;
        }
        if (d.height < 0) {
            d.height = 0;
        }
    }

    public static void shrink(
            Rectangle r,
            Insets insets
    ) {
        r.x += insets.left;
        r.y += insets.top;
        r.width -= insets.left + insets.right;
        r.height -= insets.top + insets.bottom;
    }

    /**
     * 设置JFrame的位置属性。
     * <p>如果参数{@code bounds}是{@code null}，或者包含了不合理的值则直接返回。</p>
     *
     * @param frame
     *         待设置的JFrame。
     * @param bounds
     *         JFrame的位置属性。
     * @throws IllegalArgumentException
     *         如果参数{@code frame}是{@code null}
     */
    public static void setFrameBounds(
            JFrame frame,
            FrameBounds bounds
    ) {
        notNull(frame, "frame");

        if (bounds == null) {
            return;
        }

        final Dimension sd = Toolkit.getDefaultToolkit().getScreenSize();
        shrink(sd, 32, 32);
        final Rectangle scr = new Rectangle(sd);
        final Point location = bounds.getLocation();
        final Dimension size = bounds.getSize();

        final boolean locationIsValid = scr.contains(location);
        final boolean sizeIsValid = size.width >= 32 && size.height >= 32;

        if (!locationIsValid && !sizeIsValid) {
            // 如果位置和尺寸都不合理，那么不设置位置属性。
        } else if (!locationIsValid) {
            // 位置不合理，只设置尺寸。
            frame.setSize(size);
        } else if (!sizeIsValid) {
            // 尺寸不合理，只设置位置。
            frame.setLocation(location);
        } else {
            // 都合理，设置位置和尺寸。
            frame.setBounds(bounds.getLeft(), bounds.getTop(), bounds.getWidth(), bounds.getHeight());
        }

        frame.setExtendedState(bounds.getStatus());
    }

    /**
     * 显示模态对话框并返回结果。
     *
     * @param owner
     *         模块对话框的所有者。
     * @param dialogPane
     *         对话框面板。
     * @return 模态对话框的返回结果。
     * @throws IllegalArgumentException
     *         如果参数{@code dialogPane}是{@code null}。
     */
    public static int showModel(
            Window owner,
            DialogPane dialogPane
    ) {
        notNull(dialogPane, "dialogPane");

        assertUIThread();

        if (owner == null) {
            owner = Application.mainFrame();
        }

        // 创建用于模态显示的对话框。
        final JDialog dialog = new JDialog(owner);
        dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        dialog.setModalityType(Dialog.ModalityType.DOCUMENT_MODAL);
        dialog.setModalExclusionType(Dialog.ModalExclusionType.NO_EXCLUDE);

        dialogPane.setOwner(dialog);

        dialog.setVisible(true);

        return dialogPane.getResult();
    }

//    /**
//     * 调整组件位置，移动到容器的正中间。
//     * <p>此处的容器仅用于定位，可以不是组件所属的容器。</p>
//     *
//     * @param container
//     *         容器。
//     * @param component
//     *         组件。
//     */
//    public static void autoCenterComponent(
//            Component container,
//            Component component
//    ) {
//        notNull(container, "container");
//        notNull(component, "component");
//
//        final Point p = center(component.getSize(), container.getSize());
//        SwingUtilities.convertPointToScreen(p, container);
//        component.setLocation(p);
//    }

    /**
     * 获取ListItem的值。
     *
     * @param listItem
     *         选项。
     * @return 如果参数是 {@link ListItem} 则返回它的值，否则直接转化为字符串。
     * @see ListItem#getText()
     */
    private static String mapListItemValue(
            Object listItem
    ) {
        if (listItem == null) {
            return null;
        } else if (listItem instanceof ListItem) {
            return ((ListItem) listItem).getValue();
        } else {
            return listItem.toString();
        }
    }

    /**
     * 获取列表框的选中项。
     *
     * @param listBox
     *         列表框。
     * @return 所有选中项的值，如果选中项不是 {@link ListItem} ，则通过{@link Object#toString()}获取值。
     * @throws IllegalArgumentException
     *         如果参数{@code listBox}是{@code null}。
     * @see ListItem#getValue()
     */
    public static String[] getSelectedItems(
            JList<?> listBox
    ) {
        notNull(listBox, "listBox");

        return listBox.getSelectedValuesList()
                      .stream()
                      .map(ComponentUtils::mapListItemValue)
                      .filter(Objects::nonNull).toArray(String[]::new);
    }

    /**
     * 获取列表框的选中项。
     *
     * @param dropDown
     *         下拉框。
     * @return 所有选中项的值，如果选中项不是 {@link ListItem} ，则通过{@link Object#toString()}获取值。
     * @throws IllegalArgumentException
     *         如果参数{@code dropDown}是{@code null}。
     * @see ListItem#getValue()
     */
    public static String[] getSelectedItems(
            JComboBox<?> dropDown
    ) {
        notNull(dropDown, "dropDown");

        return stream(dropDown.getSelectedObjects())
                .map(ComponentUtils::mapListItemValue)
                .filter(Objects::nonNull).toArray(String[]::new);
    }

    /**
     * 设置列表框的选中项。
     *
     * @param listBox
     *         列表框。
     * @param selectedItems
     *         选中项，如果此参数是{@code null}则取消选中所有项。
     * @throws IllegalArgumentException
     *         如果参数{@code listBox}是{@code null}。
     */
    public static void setSelectedItems(
            JList<?> listBox,
            String... selectedItems
    ) {
        notNull(listBox, "listBox");

        if (selectedItems == null) {
            listBox.clearSelection();
            return;
        }

        final ListModel<?> model = listBox.getModel();
        final List<Integer> selectedIndices = new ArrayList<>();
        for (int i = 0; i < model.getSize(); ++i) {
            if (ArrayUtils.contains(selectedItems, mapListItemValue(model.getElementAt(i)))) {
                selectedIndices.add(i);
            }
        }

        listBox.setSelectedIndices(selectedIndices.stream().mapToInt(Integer::intValue).toArray());
    }

    /**
     * 设置组合框的选中项。
     *
     * @param dropDown
     *         下拉框。
     * @param selectedItems
     *         选中项，如果此参数是{@code null}则取消选中所有项。
     * @throws IllegalArgumentException
     *         如果参数{@code dropDown}是{@code null}。
     */
    public static void setSelectedItems(
            JComboBox<?> dropDown,
            String... selectedItems
    ) {
        notNull(dropDown, "dropDown");

        if (selectedItems == null) {
            dropDown.setSelectedIndex(-1);
            return;
        }

        final ListModel<?> model = dropDown.getModel();
        int selectedIndex = -1;
        for (int i = 0; i < model.getSize(); ++i) {
            if (ArrayUtils.contains(selectedItems, mapListItemValue(model.getElementAt(i)))) {
                selectedIndex = i;
            }
        }

        dropDown.setSelectedIndex(selectedIndex);
    }

    /**
     * 更新指定下拉框的所有选项。
     * <p>原有的选项全部被删除，然后逐个添加新的选项。</p>
     *
     * @param comboBox
     *         下拉框。
     * @param items
     *         新的选项数组。
     */
    public static void updateDropDown(
            JComboBox<ListItem> comboBox,
            ListItem... items
    ) {
        notNull(comboBox, "comboBox");
        notNull(items, "items");

        assertUIThread();

        final ComboBoxModel<ListItem> model = comboBox.getModel();

        // 记录原来的选中项。
        final Object selectedItem = comboBox.getSelectedItem();

        // 如果原来的所有选项中包含了全部，那么新的列表也要包含全部。
        final ListItem firstItem = model.getElementAt(0);
        boolean includeAll = firstItem != null && String.valueOf(Integer.MAX_VALUE).equals(firstItem.getValue());

        // 构造新的列表。
        final DefaultComboBoxModel<ListItem> comboBoxModel = new DefaultComboBoxModel<>(items);
        if (includeAll) {
            comboBoxModel.insertElementAt(new ListItem(
                    UIControllers.STANDARD.getMessage("item-all"), Integer.MAX_VALUE), 0);
        } else {
            // 否则自动创建一个空白选项。
            comboBoxModel.insertElementAt(new ListItem("", ""), 0);
        }

        // 使用新列表。
        comboBox.setModel(comboBoxModel);

        // 恢复原来的选中项。
        comboBox.setSelectedItem(selectedItem);
    }

    /**
     * 更新指定列表框的所有选项。
     * <p>原有的选项全部被删除，然后逐个添加新的选项。</p>
     *
     * @param listBox
     *         列表框。
     * @param items
     *         新的选项数组。
     */
    public static void updateListBox(
            JList<ListItem> listBox,
            ListItem... items
    ) {
        notNull(listBox, "listBox");
        notNull(items, "items");

        assertUIThread();

        // 记录原来的选中项。
        final List<ListItem> selectedItems = listBox.getSelectedValuesList();

        // 构造新的列表。
        final DefaultListModel<ListItem> listModel = new DefaultListModel<>();
        final List<Integer> selectedIndices = new ArrayList<>();
        int i = 0;
        for (final ListItem item : items) {
            if (item != null) {
                listModel.addElement(item);

                if (selectedItems.contains(item)) {
                    selectedIndices.add(i);
                }

                ++i;
            }
        }

        // 使用新列表。
        listBox.setModel(listModel);

        // 恢复原来的选中项。
        if (!selectedIndices.isEmpty()) {
            listBox.setSelectedIndices(selectedIndices.stream().mapToInt(Integer::intValue).toArray());
        }
    }

    public static void highlight(
            JComponent component
    ) {
        if (component == null) {
            LoggerFactory.getLogger(ComponentUtils.class).warn("component is null!");
            return;
        }

        if (!component.isVisible()) {
            LoggerFactory.getLogger(ComponentUtils.class).warn("component is not visible!");
            return;
        }

        component.setBorder(BorderFactory.createLineBorder(Color.BLUE));

        final Container parent = component.getParent();
        if (!(parent instanceof JComponent)) {
            LoggerFactory.getLogger(ComponentUtils.class).warn("cannot find parent component");
            return;
        }

        if (!parent.isVisible()) {
            LoggerFactory.getLogger(ComponentUtils.class).warn("parent is not visible!");
            return;
        }

        ((JComponent) parent).setBorder(BorderFactory.createLineBorder(Color.RED));
    }

    /**
     * 组合边框。
     * <p>如果外部边框是{@code null}则返回内部边框，如果内部边框是{@code null}则返回外部边框，如果都是{@code null}则返回{@code null}，
     * 如果都不是{@code null}则返回内外边框组合后的{@link CompoundBorder}。</p>
     *
     * @param outside
     *         外部边框。
     * @param inside
     *         内部边框。
     * @return 组合后的边框。
     */
    public static Border combineBorders(
            Border outside,
            Border inside
    ) {
        if (outside == null && inside == null) {
            return null;
        } else if (outside == null) {
            return inside;
        } else if (inside == null) {
            return outside;
        } else {
            return BorderFactory.createCompoundBorder(outside, inside);
        }
    }

    public static void focusInWindow(
            Component component
    ) {
        notNull(component, "component");

        if (!component.isVisible()) {
            // 不可见的组件无法获取焦点。
            return;
        }

        AccessibleContext ac = component.getAccessibleContext();

        while (ac != null) {
            final AccessibleComponent accessibleComponent = ac.getAccessibleComponent();
            if (accessibleComponent == null) {
                break;
            }

            Accessible a = ac.getAccessibleParent();
            if (a instanceof JTabbedPane) {
                final int index = ac.getAccessibleIndexInParent();
                JTabbedPane tabbedPane = (JTabbedPane) a;
                if (tabbedPane.getSelectedIndex() != index) {
                    tabbedPane.setSelectedIndex(index);
                }
            }

            ac = a != null ? a.getAccessibleContext() : null;
        }

        component.requestFocusInWindow();
    }

    public static Color lighterColor(
            Color c
    ) {
        notNull(c, "c");

        final double FACTOR = 2.8;

        int r = c.getRed();
        int g = c.getGreen();
        int b = c.getBlue();
        int alpha = c.getAlpha();

        r = (int) (r + (255 - r) / FACTOR);
        g = (int) (g + (255 - g) / FACTOR);
        b = (int) (b + (255 - b) / FACTOR);

        return new Color(Math.min(r, 255), Math.min(g, 255), Math.min(b, 255), alpha);
    }

    public static void addChangeListener(
            JTextComponent component,
            Consumer<DocumentEvent> listener
    ) {
        notNull(component, "component");
        notNull(listener, "listener");

        final DocumentListener documentListener = new DocumentListener() {
            private int lastChange = 0, lastNotifiedChange = 0;

            @Override
            public void insertUpdate(
                    DocumentEvent event
            ) {
                changedUpdate(event);
            }

            @Override
            public void removeUpdate(
                    DocumentEvent event
            ) {
                changedUpdate(event);
            }

            @Override
            public void changedUpdate(
                    DocumentEvent event
            ) {
                lastChange++;
                SwingUtilities.invokeLater(() -> {
                    if (lastNotifiedChange != lastChange) {
                        lastNotifiedChange = lastChange;
                        listener.accept(event);
                    }
                });
            }
        };
        component.addPropertyChangeListener("document", event -> {
            final Document oldDocument = (Document) event.getOldValue();
            final Document newDocument = (Document) event.getNewValue();
            if (oldDocument != null) oldDocument.removeDocumentListener(documentListener);
            if (newDocument != null) newDocument.addDocumentListener(documentListener);
            documentListener.changedUpdate(null);
        });

        final Document document = component.getDocument();
        if (document != null) {
            document.addDocumentListener(documentListener);
        }
    }
}
